package jmine.tec.utils.version;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import jmine.tec.utils.Tuple;
import jmine.tec.utils.UnmodifiableTuple;

import org.apache.log4j.Logger;

/**
 * Representa um componente (tipicamente um jar de dependencias)
 * 
 * @author takeshi
 */
public final class MavenComponent implements Serializable {

    private final String groupId, artifactId, versionString;

    private final String url;

    private final Version version;

    /**
     * C'tor
     * 
     * @param groupId String
     * @param artifactId String
     * @param versionString String
     * @param scmRev String
     * @param url String
     */
    private MavenComponent(String groupId, String artifactId, String versionString, String scmRev, String url) {
        super();
        this.url = url;
        this.groupId = groupId;
        this.artifactId = artifactId;
        this.versionString = versionString;
        if (scmRev == null || scmRev.length() == 0) {
            this.version = new Version(this.versionString, SCMRevision.NA);
        } else {
            this.version = new Version(this.versionString, new SCMRevision(scmRev));
        }
    }

    /**
     * @return {@link String} the version string
     */
    public String getVersionString() {
        return this.versionString;
    }

    /**
     * @param groupId String
     * @param artifactId String
     * @return {@link MavenComponent}
     */
    public static MavenComponent getComponent(String groupId, String artifactId) {
        MavenComponent version = findBuildInfo(groupId, artifactId);
        if (version == null) {
            version = findPomProperties(groupId, artifactId);
        }
        if (version == null) {
            version = createComponent(groupId, artifactId, "Unknown", "", "");
        }
        return version;
    }

    /**
     * Procura o {@link MavenComponent} que possui a classe passada. Este eh o unico metodo que pode devolver null
     * 
     * @param cls Class
     * @return {@link MavenComponent}
     */
    public static MavenComponent forClass(Class<?> cls) {
        if (cls.isMemberClass()) {
            return forClass(cls.getEnclosingClass());
        }
        URL location = cls.getProtectionDomain().getCodeSource().getLocation();
        try {
            if (location.getProtocol().equals("file")) {
                File source = new File(location.toURI());
                if (source.isDirectory()) {
                    return fromDirectory(source);
                } else if (source.getName().toLowerCase().endsWith(".jar") || source.getName().toLowerCase().endsWith(".zip")) {
                    return fromJarFile(source);
                } else {
                    Logger.getLogger(MavenComponent.class).warn("Unsupported file: " + source);
                    return null;
                }
            }
        } catch (URISyntaxException e1) {
            Logger.getLogger(MavenComponent.class).warn("Unsupported file: " + location, e1);
        }
        return null;
    }

    /**
     * @param source File
     * @return {@link MavenComponent} or <code>null</code>
     */
    private static MavenComponent fromJarFile(File source) {
        try {
            ZipFile jarFile = new ZipFile(source);
            try {
                Enumeration<? extends ZipEntry> entries = jarFile.entries();
                while (entries.hasMoreElements()) {
                    ZipEntry entry = entries.nextElement();
                    String entryName = entry.getName();
                    if (!entry.isDirectory() && entryName.endsWith("/pom.properties") && entryName.contains("META-INF/maven/")) {
                        Properties props = new Properties();
                        props.load(jarFile.getInputStream(entry));
                        return propertiesToComponent(props, source.getAbsolutePath());
                    }
                }
            } finally {
                jarFile.close();
            }
        } catch (IOException e) {
            Logger.getLogger(MavenComponent.class).warn("Error reading file: " + source, e);
        }
        return null;
    }

    /**
     * @param source File
     * @return {@link MavenComponent}
     */
    private static MavenComponent fromDirectory(File source) {
        File metaInf = new File(source, "META-INF");
        if (!metaInf.exists()) {
            return null;
        }
        File maven = new File(metaInf, "maven");
        if (!maven.exists()) {
            return null;
        }
        File[] listFiles = maven.listFiles();
        for (File file : listFiles) {
            File pom = new File(file, "pom.properties");
            if (pom.exists()) {
                try {
                    FileInputStream fin = new FileInputStream(pom);
                    try {
                        Properties props = new Properties();
                        props.load(fin);
                        return propertiesToComponent(props, pom.getAbsolutePath());
                    } finally {
                        fin.close();
                    }
                } catch (IOException e) {
                    Logger.getLogger(MavenComponent.class).warn("Error reading file: " + pom, e);
                }
            }
        }
        return null;
    }

    /**
     * Procura o pom.properties
     * 
     * @param groupId2 String
     * @param artifactId2 String
     * @return {@link MavenComponent}
     */
    private static MavenComponent findPomProperties(String groupId2, String artifactId2) {
        Tuple<Properties, String> ts =
                findProperties(groupId2, artifactId2, "META-INF/maven/" + artifactId2 + "/" + groupId2 + "/pom.properties");
        if (ts != null) {
            return propertiesToComponent(ts.head(), ts.tail());
        }
        return null;
    }

    /**
     * Procura o arquivo de buildInfo e transforma num properties
     * 
     * @param cl ClassLoader
     * @param groupId String
     * @param artifactId String
     * @return Properties ou null
     * @param resName o nome do recurso
     */
    private static Tuple<Properties, String> findBuildInfo(ClassLoader cl, String groupId, String artifactId, String resName) {
        if (cl == null) {
            return null;
        }

        try {
            Enumeration<URL> resources = cl.getResources(resName);
            while (resources.hasMoreElements()) {
                URL url = resources.nextElement();
                try {
                    Properties props = new Properties();
                    props.load(url.openStream());
                    if (groupId.equals(props.get("groupId")) && artifactId.equals(props.get("artifactId"))) {
                        return UnmodifiableTuple.newTuple(props, url.toString());
                    }
                } catch (IOException e) {
                    Logger.getLogger(MavenComponent.class).warn("IOException trying to read file: " + url, e);
                }
            }
        } catch (IOException e) {
            Logger.getLogger(MavenComponent.class).warn("Generic IOException trying to get classpath resource: " + resName, e);
        }
        return null;
    }

    /**
     * Procura por um arquivo gerado pelo buildInfo plugin
     * 
     * @param groupId2 String
     * @param artifactId2 String
     * @return {@link MavenComponent}
     */
    private static MavenComponent findBuildInfo(String groupId2, String artifactId2) {
        String resName = "buildInfo-" + artifactId2 + ".properties";
        Tuple<Properties, String> ts = findProperties(groupId2, artifactId2, resName);
        if (ts != null) {
            return propertiesToComponent(ts.head(), ts.tail());
        }
        return null;
    }

    /**
     * Transforma um properties e um URL em um {@link MavenComponent}
     * 
     * @param props {@link Properties}
     * @param url String
     * @return {@link MavenComponent}
     */
    private static MavenComponent propertiesToComponent(Properties props, String url) {
        return createComponent(props.getProperty("groupId"), props.getProperty("artifactId"), props.getProperty("version"),
                props.getProperty("revision"), url);
    }

    /**
     * Procura o arquivo de buildInfo - ou seja, soh vai funcionar para componentes nossos
     * 
     * @param groupId2 String
     * @param artifactId2 String
     * @param resName String
     * @return Tuple
     */
    private static Tuple<Properties, String> findProperties(String groupId2, String artifactId2, String resName) {
        Tuple<Properties, String> props = null;
        if (Thread.currentThread().getContextClassLoader() != null) {
            props = findBuildInfo(Thread.currentThread().getContextClassLoader(), groupId2, artifactId2, resName);
        }
        if (props == null) {
            props = findBuildInfo(Comparable.class.getClassLoader(), groupId2, artifactId2, resName);
        }
        return props;
    }

    /**
     * @param groupId String
     * @param artifactId String
     * @param versionString String
     * @param scmRevision String
     * @param url2 String
     * @return {@link MavenComponent}
     */
    private static MavenComponent createComponent(String groupId, String artifactId, String versionString, String scmRevision, String url2) {
        return new MavenComponent(groupId, artifactId, versionString, scmRevision, url2);
    }

    /**
     * @return String
     */
    public String groupId() {
        return this.groupId;
    }

    /**
     * @return String
     */
    public String artifactId() {
        return this.artifactId;
    }

    /**
     * @return String
     */
    public Version version() {
        return this.version;
    }

    /**
     * @return the url
     */
    public String getUrl() {
        return this.url;
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#toString()
     */

    @Override
    public String toString() {
        return "MavenComponent [artifactId=" + this.artifactId + ", groupId=" + this.groupId + ", url=" + this.url + ", version="
                + this.version + ", versionString=" + this.versionString + "]";
    }

}
